<?php
class Phad {
// contains submit(), delete() & utility functions for these
use Phad\Submission;
// idk, does routing stuff?
use Phad\Routes;
// contains can_read_data(), can_read_row(), and can_delete()
use Phad\Can_Do;
// contains extra utility functions
use Phad\Utility;
use Phad\SpamControl;
public ?\PDO $pdo = null;
public $route_prefix = '';
/**
* array of configs, typically from an on-disk json file. These are not directly used by Phad, but may be useful to subclasses or integrations.
*/
public array $configs = [];
/**
* sitemap builder instance
*/
public \Phad\SitemapBuilder $sitemap;
/**
* a router instance
*/
public \Lia\Addon\Router $router;
/**
* Callback handlers. Key should be the handler name & value should be a callable
*/
public $handlers = [
/** should accept a single string representing the role (or roles) being checked.
* As a suggestion, roles should be like 'guest' or 'admin|moderator|vip' for multiple */
'user_has_role'=>null,
/** should accept a single ItemInfo object (stdClass) */
'item_initialized'=>null,
];
/**
* array of callables that return an array of rows.
* Each callable should accept args `(DomNode, ItemInfo)` & return an array of rows.
*/
public $data_loaders = [];
/**
* set false to stop phad from `exit`ing when calling `->redirect()`
* You can custom handle the `header()` call by creating a `header()` function in the `Phad` namespace.
*
* See https://akrabat.com/replacing-a-built-in-php-function-when-testing-a-component/ to understand the `Phad\header()` thing
*/
public bool $exit_on_redirect = true;
/**
* true to always re-compile views
*/
public bool $force_compile = false;
/**
* args to pass to every phad view
*/
public $global_phad_args = [];
/**
* `key=>value` array of filters where `key` is the filter you write in the html & `value` is a callable
* @feature(ValueFilter) Create a filter by setting `$phad->filters['filter_name'] = callable;`, then declare `<section prop="body" filter="filter_name"`></section>` to use it
*/
public $filters = [
// 'commonmark:markdownToHtml'=>'markdownFilter',
];
/** Absolute path to a directory that contains phad items */
public $item_dir;
/** Absolute path to a directory to store cached files */
public $cache_dir;
/** Dir to write sitemap.xml file to */
public $sitemap_dir;
/** true to throw exception when query failes. false to silently fail & return false */
public bool $throw_on_query_failure = false;
/** an object that is used to make phad work with liaison */
public $integration;
/** array of functions that handle access operations */
public $access_handlers = [];
/**
* array of handlers for sitemap building
*/
public $sitemap_handlers = [];
/** array of validation functions
* @key should correspond to a `validate="key"` attribute on an html node.
* @value should be a function with the signature `function($property_value, $property_settings, &$errors): bool`
*/
public $validators = [];
public function __construct(){
$this->handlers['item_initialized'] = function(){};
if (class_exists('\League\CommonMark\CommonMarkConverter',true)){
$this->filters['commonmark:markdownToHtml']=[$this,'filter_markdown'];
}
}
public function __call($method,$args){
if (empty($this->handlers[$method])){
throw new \BadMethodCallException("Handler '$method' is not set on Phad. do \$phad->handlers['$method'] = function(...){};\n");
}
$callable = $this->handlers[$method];
return $callable(...$args);
}
/**
* Use the given handler for hooks phad requires you to handle
*/
public function set_handler(\Phad\Handler $handler){
$this->handlers['user_has_role'] = [$handler, 'user_has_role'];
$this->handlers['can_read_row'] = [$handler, 'can_read_row'];
}
/**
* @return an array of rows
*
* Options: args includes `['ItemName'=>$row]` or `['ItemNameList'=>[$row1,$row2,$r3,....]` or pass in args required by your data node to run a query
*/
public function read_data($node, $ItemInfo){
// var_dump($node);
if ($node['type']=='default'){
$name = $ItemInfo->name;
if (isset($ItemInfo->args[$name]))return [$ItemInfo->args[$name]];
else if (isset($ItemInfo->args[$name.'List'])) return $ItemInfo->args[$name.'List'];
else if ($ItemInfo->type=='form'&&$ItemInfo->mode==\Phad\Blocks::FORM_SUBMIT){
return [$_POST];
} else if ($ItemInfo->type=='form'&&isset($_GET['id'])){
// echo 'zeep';exit;
}
else if ($ItemInfo->type=='form'){
// `object_from_row()` will then create `BlackHole` object
return [['_object'=>'Phad\\BlackHole']];
}
} else if (isset($node['data_loader'])){
$key = $node['data_loader'];
if (!isset($this->data_loaders[$key]))throw new \Exception("There is no data loader for key '$key'. Set `\$phad->data_loaders['$key']` to a callable that accepts (DOMNode, ItemInfo) & returns an array of rows.");
$rows = $this->data_loaders[$key]($node, $ItemInfo);
$final_rows = [];
foreach ($rows as $row){
if ($this->can_read_row($row, $ItemInfo, $ItemInfo->name))$final_rows[] = $row;
}
return $final_rows;
}
$query = new \Phad\Query();
$query->pdo = &$this->pdo;
$query->throw_on_query_failure = &$this->throw_on_query_failure;
$node = $this->modify_query_info($node);
$rows = $query->get($ItemInfo->name, $node, $ItemInfo->args, $ItemInfo->type, $node);
if ($rows===false){
// echo 'no rows';
// exit;
return [];
}
$final_rows = [];
foreach ($rows as $row){
if ($this->can_read_row($row, $ItemInfo, $ItemInfo->name))$final_rows[] = $row;
}
return $final_rows;
}
/**
* Modify a query before getting rows
*
* @param $query_info an array with keys sql, limit, orderby, where, etc... if 'sql' is set, the others are ignored. See \Phad\Query->buildSql() for more infromation.
*
* @override to provide custom query modifications
*
* @return array of query info
*/
public function modify_query_info(array $query_info){
return $query_info;
}
/**
* @override
*/
public function object_from_row(array $row, $ItemInfo){
//@issue(jan 31, 2022) a jank "fix" bc i don't have union types in php 7.4 & the union would be for an `ArrayObject|array`
if (isset($row['_object'])&&$row['_object']=='Phad\\BlackHole')return new \Phad\BlackHole();
return (object)$row;
}
public function has_item($name){
return file_exists($this->item_dir.'/'.$name.'.php');
}
public function item($name, $args=[]){
$args['phad'] = $args['phad'] ?? $this;
$args['is_route'] = $args['is_route'] ?? false;
foreach ($this->global_phad_args as $k=>$v){
if (!isset($args[$k]))$args[$k] = $v;
}
$item = new \Phad\Item($name, $this->item_dir, $args);
$item->force_compile = $this->force_compile;
return $item;
}
/**
* get an item instance from a file
*
* This does not set up any routing
*/
public function item_from_file(string $file_path, array $args=[]){
$args['phad'] = $args['phad'] ?? $this;
foreach ($this->global_phad_args as $k=>$v){
if (!isset($args[$k]))$args[$k] = $v;
}
// remove .php from the file path, for the item 'name'
$item = new \Phad\Item(substr(basename($file_path),0,-4), dirname($file_path), $args);
$item->templateFile = $file_path;
return $item;
}
/**
*
* @param $filterName the name of the filter to pass `$value` through
* @param $value the value you wish to modify
*
* @throws if `$filterName` is not set
* @throws if `$filterName` points to a non-callable
*/
public function filter(string $filterName, $value){
//conditional namespacing would be nice, so the ns: prefix can be left off when there are no conflicts
if (!isset($this->filters[$filterName])){
throw new \Exception("Filter '{$filterName}' is not set.");
}
$filter = $this->filters[$filterName];
if (!is_callable($filter)){
throw new \Exception("Filter '{$filterName}' is not callable.");
}
$filtered = $filter($value);
return $filtered;
}
/**
* Apply commonmark conversion to the value, turning markdown into html
* @param $markdown the value which is markdown and should become html
*/
public function filter_markdown($markdown){
$converter = new \League\CommonMark\CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
if (method_exists($converter,'convert')){
$html = $converter->convert($markdown);
} else {
$html = $converter->convertToHtml($markdown);
}
return $html;
}
/**
* @override to customize how item rows are returned
*/
public function get_rows($ItemInfo){
$rows = [];
foreach ($ItemInfo->rows as $row){
if ($this->can_read_row($row, $ItemInfo, $ItemInfo->name)){
$rows[] = $row;
}
}
return $rows;
}
/**
* Just boilerplate to make phad easier to initialize
*/
static public function main($options = [], $custom_phad=null){
$class = static::class;
$phad = $custom_phad ?? new $class();
$phad->configs = $options;
foreach ($options as $k=>$v){
$phad->$k = $v;
}
$phad->sitemap = new \Phad\SitemapBuilder($phad->sitemap_dir);
$phad->sitemap->cache_dir = &$phad->cache_dir;
$phad->sitemap->pdo = &$phad->pdo;
$phad->sitemap->throw_on_query_failure = &$phad->throw_on_query_failure;
$phad->sitemap->router = &$phad->router;
$phad->sitemap->handlers = &$phad->sitemap_handlers;
// $phad->filter = new \Phad\Filter();
// $phad->access = new \Phad\Access();
// $phad->access->pdo = &$phad->pdo;
// $phad->access->throw_on_query_failure = &$phad->throw_on_query_failure;
// $phad->access->user = &$phad->user;
// $phad->stack = new \Phad\Stack();
// $phad->form = new \Phad\FormValidator();
// $phad->form->validators = &$phad->validators;
// $phad->form->throw_submission_error = &$phad->throw_submission_error;
// $phad->submitter = new \Phad\PDOSubmitter();
// $phad->submitter->pdo = &$phad->pdo;
// $phad->submitter->lildb = new \Tlf\LilDb($phad->pdo);
$phad->integration = new \Phad\Integration();
$phad->integration->phad = $phad;
$phad->integration->force_compile = &$phad->force_compile;
return $phad;
}
/**
* Make a sitemap file from all views
* @todo add caching of the sitemap file
* @return path to the new sitemap file
*/
public function create_sitemap_file(): string{
if (file_exists($this->sitemap_dir.'/sitemap.xml')){
unlink($this->sitemap_dir.'/sitemap.xml');
}
$items = $this->get_all_items();
$sm_builder = $this->sitemap;
$sm_list = $sm_builder->get_sitemap_list($items, $this);
$sm_builder->make_sitemap($sm_list);
return $this->sitemap_dir.'/sitemap.xml';
}
public function compile_all_items(){
$items = $this->get_all_items();
foreach ($items as $i){
$item = $this->item($i,[]);
$item->compile();
}
}
/**
*
* call a method that's defined in strings like `call:check_is_2022` (would call `$this->access_handler['check_is_2022']` if it is set
*
* @param $handler the name of the access_handler to call
* @param ...$args a list of args to pass to the access_handler
* @throws exception if access handler is not set
* @todo rename access_handlers to call_handlers
*/
public function call($handler, ...$args){
if (!isset($this->access_handlers[$handler])){
throw new \Exception("No access handler set for '$handler'. Do \$phad->access_handlers['$handler'] = `function(...\$args){}`\nIt should return true/false for access/candelete. Argslist varies by caller");
}
$callable = $this->access_handlers[$handler];
// $count = count($Info->submit_errors);
return $callable(...$args);
}
/**
* parse a string like `print:woohoo;call:somethin;role:admin;`
* @return array with key=function and value=args like `key=print` & `value=woohoo`
*/
public function parse_functions(?string $call_string){
if ($call_string===null)return [];
$functions = explode(';', $call_string);
if (count($functions)==1&&trim($functions[0])=='')return [];
// print_r($functions);
// var_dump($call_string);
// exit;
foreach ($functions as $f){
$f = trim($f);
if ($f=='')continue;
$parts = explode(':', $f, 2);
$method = $parts[0];
$arg = $parts[1];
$out[$method] = $arg;
}
return $out;
}
/**
* Handle when no rows were loaded. This method is for overriding and does literally nothing otherwise.
*
* @param $ItemInfo an item info object
* @output is optional and likely would contain some kind of error message.
*/
public function no_rows_loaded(stdClass $ItemInfo){
}
/**
* Handle when a node cannot be read
*
* @param $node the node info
* @output is optional and may contain some kind of error output
*/
public function read_node_failed(array $node){
}
/**
* replace each `$value` with `htmlspecialchars($value)`
* @param &$row a row of data, ideally prior to submission
*/
public function sanitize_user_row(array &$row){
foreach (array_keys($row) as $key){
if (!is_numeric($key));
$row[$key] = htmlspecialchars($row[$key]);
}
}
}